SPB Git

Repository Public

page.tsx 8.1 KB 151 lines tsx

1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { NodeLivePanel } from "@/components/live/NodeLive";5import { chipShort, LifecycleBadge } from "@/components/nodes/bits";6import { Photo } from "@/components/ui/Photo";7import { nodeById, nodes, photoById, publishedBenchmarks } from "@/lib/data";8import { formatDate, getDict, LOCALES, type Locale } from "@/lib/i18n";9import { pageMetadata } from "@/lib/seo";1011type Props = { params: Promise<{ locale: Locale; id: string }> };1213function Rows({ rows }: { rows: [string, React.ReactNode][] }) {14  return (15    <dl className="divide-y divide-line border-y border-line">16      {rows.map(([k, v]) => (17        <div key={k} className="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)] gap-4 py-3.5">18          <dt className="text-[0.875rem] text-mute">{k}</dt>19          <dd className="num text-[0.9375rem]">{v}</dd>20        </div>21      ))}22    </dl>23  );24}2526export const dynamicParams = false;27export const generateStaticParams = () => LOCALES.flatMap((locale) => nodes.map((n) => ({ locale, id: n.id })));2829export async function generateMetadata({ params }: Props): Promise<Metadata> {30  const { locale, id } = await params;31  const node = nodeById(id);32  if (!node) return {};33  const t = getDict(locale);34  return pageMetadata(locale, "nodes", `/nodes/${id}`, { title: `${node.publicName} — ${t.common.families[node.family]}${node.chip ? ` ${chipShort(node.chip)}` : ""}`, description: node.description[locale] });35}3637export default async function NodePage({ params }: Props) {38  const { locale, id } = await params;39  const node = nodeById(id);40  if (!node) notFound();41  const t = getDict(locale);42  const d = t.nodes.detail;43  const nd = <span className="text-mute">{t.common.notDocumented}</span>;44  const idx = nodes.findIndex((n) => n.id === node.id);45  const prev = nodes[idx - 1];46  const next = nodes[idx + 1];47  const photo = node.imageIds.map(photoById).find(Boolean);48  const bench = publishedBenchmarks.filter((b) => b.nodeId === node.id);49  const gb = (v: number | null) => (v !== null ? `${v} ${t.common.gb}` : nd);5051  const specs: [string, React.ReactNode][] = [52    [d.family, t.common.families[node.family]],53    [d.model, node.modelIdentifier ?? nd],54    [d.chip, node.chip ?? nd],55    [d.cpu, node.cpuCores ?? nd],56    [d.layout, node.cpuCoreLayout ?? nd],57    [d.gpu, node.gpuCores ?? nd],58    [d.memory, gb(node.unifiedMemoryGB)],59    [d.storage, gb(node.storageGB)],60    [d.network, node.networkInterface ? (locale === "fr" ? node.networkInterface.replace("Ethernet (1000BASE-T, negotiated link)", "Ethernet (1000BASE-T, lien négocié)") : node.networkInterface) : nd],61  ];62  const placement: [string, React.ReactNode][] = [63    [d.rack, node.rack ? t.nodes.filters.rackLabel(node.rack) : nd],64    [d.shelf, node.shelf ?? nd],65    [d.added, node.addedAt ? <time dateTime={node.addedAt}>{formatDate(node.addedAt, locale)}</time> : nd],66  ];67  return (68    <article className="shell pt-8 md:pt-14">69      <p><Link className="link text-[0.875rem]" href={`/${locale}/nodes`}>← {t.common.back}</Link></p>70      <header className="mt-7 grid gap-6 pb-8 md:grid-cols-12 md:pb-10">71        <div className="md:col-span-8">72          <p className="eyebrow">{d.eyebrow} · {String(node.number).padStart(2, "0")}</p>73          <h1 className="display mt-5 text-[clamp(2.75rem,1.4rem+6vw,5.5rem)]">{node.publicName}</h1>74          <p className="mt-5 text-xl tracking-[-0.01em] text-body">{t.common.families[node.family]}{node.chip ? ` · ${chipShort(node.chip)}` : ""}{node.unifiedMemoryGB !== null ? ` · ${node.unifiedMemoryGB} ${t.common.gb}` : ""}</p>75        </div>76        <div className="flex flex-col items-start gap-3 md:col-span-4 md:items-end md:justify-end">77          <LifecycleBadge state={node.lifecycle} label={t.common.lifecycle[node.lifecycle]} />78          <p className="label">{t.common.verified} {node.verifiedAt ? <time dateTime={node.verifiedAt} className="text-body">{formatDate(node.verifiedAt, locale)}</time> : nd}</p>79        </div>80      </header>8182      <NodeLivePanel id={node.id} locale={locale} />8384      <div className="grid gap-14 pt-12 lg:grid-cols-12 lg:gap-12 md:pt-16">85        <div className="lg:col-span-7">86          <section aria-labelledby="specs-h">87            <h2 id="specs-h" className="label mb-4">{d.specs}</h2>88            <Rows rows={specs} />89            <p className="mt-3 text-[0.8125rem] text-mute">{d.layoutLegend}</p>90          </section>91          <section aria-labelledby="fn-h" className="mt-12">92            <h2 id="fn-h" className="label mb-4">{d.function}</h2>93            <p className="prose-tech">{node.description[locale]}</p>94            <ul className="mt-5 flex flex-wrap gap-2">95              {node.role.map((r) => <li key={r} className="rounded-full border border-line-strong bg-surface px-3 py-1 text-[0.8125rem] text-body">{t.common.roles[r]}</li>)}96            </ul>97          </section>98          <section aria-labelledby="pl-h" className="mt-12">99            <h2 id="pl-h" className="label mb-4">{d.placement}</h2>100            <Rows rows={placement} />101          </section>102          <section aria-labelledby="hist-h" className="mt-12 grid gap-8 sm:grid-cols-2">103            <div>104              <h2 id="hist-h" className="label mb-3">{d.history}</h2>105              <p className="text-[0.9375rem] leading-relaxed text-body">{d.historyNone}</p>106            </div>107            <div>108              <h2 className="label mb-3">{d.benchmarks}</h2>109              {bench.length === 0 ? (110                <p className="text-[0.9375rem] leading-relaxed text-body">{d.benchmarksNone} <Link className="link" href={`/${locale}/benchmarks`}>{t.nav.benchmarks}</Link></p>111              ) : (112                <p className="text-[0.9375rem]"><Link className="link" href={`/${locale}/benchmarks`}>{bench.length} — {t.nav.benchmarks}</Link></p>113              )}114            </div>115          </section>116        </div>117118        <aside className="lg:col-span-5">119          <section aria-labelledby="ph-h">120            <h2 id="ph-h" className="label mb-4">{d.photo}</h2>121            {photo ? (122              <figure>123                <div className="photo-frame"><Photo photo={photo} locale={locale} sizes="(min-width: 1024px) 38vw, 100vw" className="h-auto w-full" /></div>124                <figcaption className="mt-3 text-[0.8125rem] leading-relaxed text-mute">{photo.title[locale]}. {d.photoNote}</figcaption>125              </figure>126            ) : (127              <p className="card border-dashed p-6 text-[0.9375rem] leading-relaxed text-body">{d.photoNone}</p>128            )}129          </section>130          <section aria-labelledby="prov-h" className="mt-10">131            <h2 id="prov-h" className="label mb-4">{d.provenance}</h2>132            <Rows rows={[[t.common.verified, node.verifiedAt ? formatDate(node.verifiedAt, locale) : nd], [d.status, t.common.lifecycle[node.lifecycle]]]} />133            <p className="mt-3 text-[0.8125rem] leading-relaxed text-mute" lang="en">{t.common.source}: {node.source ?? t.common.notDocumented}</p>134          </section>135          <section aria-labelledby="cav-h" className="mt-10">136            <h2 id="cav-h" className="label mb-4">{d.caveats}</h2>137            <ul className="space-y-2 text-[0.875rem] leading-relaxed text-body">138              {d.caveatItems.map((c) => <li key={c} className="flex gap-3"><span aria-hidden="true" className="mt-[0.7em] h-px w-3 shrink-0 bg-accent" />{c}</li>)}139            </ul>140          </section>141        </aside>142      </div>143144      <nav aria-label={t.nav.nodes} className="mt-20 grid grid-cols-2 gap-4 border-t border-line pt-8">145        <div>{prev ? <Link className="group block" href={`/${locale}/nodes/${prev.id}`}><span className="label">← {d.prev}</span><span className="mt-2 block text-xl font-medium group-hover:text-accent">{prev.publicName}</span></Link> : null}</div>146        <div className="text-right">{next ? <Link className="group block" href={`/${locale}/nodes/${next.id}`}><span className="label">{d.next} →</span><span className="mt-2 block text-xl font-medium group-hover:text-accent">{next.publicName}</span></Link> : null}</div>147      </nav>148    </article>149  );150}151
Files main
Menu

Theme